/*
 *
 *  *
 *  *  *
 *  *  * Copyright (c) 2008-2017 UBT Corporation.  All rights reserved.  Redistribution,
 *  *  *  modification, and use in source and binary forms are not permitted unless otherwise authorized by UBT.
 *  *  *
 *  *
 *
 */

package com.ubtech.utilcode.utils;


import android.os.Looper;

import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.Charset;
import java.util.ArrayDeque;
import java.util.Comparator;
import java.util.Iterator;
import java.util.PriorityQueue;

import static java.nio.charset.StandardCharsets.US_ASCII;

/**
 * @desc : ByteBuffer对象池
 *         1.管理一个static的reclaimed ByteBuffer队列
 *         2.BBL实例维护一个mBuffers队列
 *         3.提供接口读取mBuffers中字节
 *         4.按条件重用mBuffers中的ByteBuffer。
 * @@author: Logic
 * @email : pdlogic1987@gmail.com
 * @time : 2017/4/17
 * @modifier:
 * @modify_time:
 */
public class ByteBufferList {

  public static final ByteBuffer EMPTY_BYTEBUFFER = ByteBuffer.allocate(0);

  private static final Object LOCK = new Object();

  public static int MAX_ITEM_SIZE = 1024 * 4;
  public static int MIN_ITEM_SIZE = 1024;

  static PriorityQueue<ByteBuffer> reclaimed = new PriorityQueue<ByteBuffer>(8, new Reclaimer());

  static int currentSize = 0;

  static int maxItem = 0;

  private static int MAX_SIZE = 1024 * 64;

  ArrayDeque<ByteBuffer> mBuffers = new ArrayDeque<ByteBuffer>();

  ByteOrder order = ByteOrder.BIG_ENDIAN;

  private int remaining = 0;

  public ByteBufferList() {
  }

  public ByteBufferList(ByteBuffer... b) {
    addAll(b);
  }

  public ByteBufferList(byte[] buf) {
    super();
    ByteBuffer b = ByteBuffer.wrap(buf);
    add(b);
  }

  private static PriorityQueue<ByteBuffer> getReclaimed() {
    if (Thread.currentThread() == Looper.getMainLooper().getThread())
      return null;
    return reclaimed;
  }

  public static void setMaxPoolSize(int size) {
    MAX_SIZE = size;
  }

  public static void setMaxItemSize(int size) {
    MAX_ITEM_SIZE = size;
  }

  private static boolean reclaimedContains(ByteBuffer b) {
    for (ByteBuffer other : reclaimed) {
      if (other == b) return true;
    }
    return false;
  }


  public static void reclaim(ByteBuffer b) {
    if (b == null || b.isDirect()) return;
    if (b.arrayOffset() != 0 || b.array().length != b.capacity()) return;
    if (b.capacity() < MIN_ITEM_SIZE) return;
    if (b.capacity() > MAX_ITEM_SIZE) return;

    PriorityQueue<ByteBuffer> r = getReclaimed();
    if (r == null) return;

    synchronized (LOCK) {
      while (currentSize > MAX_SIZE && r.size() > 0 && r.peek().capacity() < b.capacity()) {
        ByteBuffer head = r.remove();
        currentSize -= head.capacity();
      }

      if (currentSize > MAX_SIZE) {
        return;
      }

      assert !reclaimedContains(b);

      b.position(0);
      b.limit(b.capacity());
      currentSize += b.capacity();
      r.add(b);
      assert r.size() != 0 ^ currentSize == 0;

      maxItem = Math.max(maxItem, b.capacity());
    }
  }

  public static ByteBuffer obtain(int size) {
    if (size <= maxItem) {
      PriorityQueue<ByteBuffer> r = getReclaimed();
      if (r != null) {
        synchronized (LOCK) {
          while (r.size() > 0) {
            ByteBuffer ret = r.remove();
            if (r.size() == 0) maxItem = 0;
            currentSize -= ret.capacity();
            assert r.size() != 0 ^ currentSize == 0;
            if (ret.capacity() >= size) {
              return ret;
            }
          }
        }
      }
    }
    ByteBuffer ret = ByteBuffer.allocate(Math.max(MIN_ITEM_SIZE, size));
    return ret;
  }

  public static void obtainArray(ByteBuffer[] arr, int size) {
    PriorityQueue<ByteBuffer> r = getReclaimed();
    int index = 0;
    int total = 0;

    if (r != null) {
      synchronized (LOCK) {
        while (r.size() > 0 && total < size && index < arr.length - 1) {
          ByteBuffer b = r.remove();
          currentSize -= b.capacity();
          assert r.size() != 0 ^ currentSize == 0;
          int needed = Math.min(size - total, b.capacity());
          total += needed;
          arr[index++] = b;
        }
      }
    }

    if (total < size) {
      ByteBuffer b = ByteBuffer.allocate(Math.max(MIN_ITEM_SIZE, size - total));
      arr[index++] = b;
    }

    for (int i = index; i < arr.length; i++) {
      arr[i] = EMPTY_BYTEBUFFER;
    }
  }

  public static void writeOutputStream(OutputStream out, ByteBuffer b) throws IOException {
    byte[] bytes;
    int offset;
    int length;
    if (b.isDirect()) {
      bytes = new byte[b.remaining()];
      offset = 0;
      length = b.remaining();
      b.get(bytes);
    } else {
      bytes = b.array();
      offset = b.arrayOffset() + b.position();
      length = b.remaining();
    }
    out.write(bytes, offset, length);
  }

  public ByteOrder order() {
    return order;
  }

  public ByteBufferList order(ByteOrder order) {
    this.order = order;
    return this;
  }

  public void addAll(ByteBuffer... bb) {
    for (ByteBuffer b : bb)
      add(b);
  }

  public byte[] getAllByteArray() {
    if (mBuffers.size() == 1) {
      ByteBuffer peek = mBuffers.peek();
      if (peek.capacity() == remaining() && peek.isDirect()) {
        remaining = 0;
        return mBuffers.remove().array();
      }
    }
    byte[] ret = new byte[remaining()];
    get(ret);
    return ret;
  }

  public ByteBuffer[] getAllArray() {
    ByteBuffer[] ret = new ByteBuffer[mBuffers.size()];
    ret = mBuffers.toArray(ret);
    mBuffers.clear();
    remaining = 0;
    return ret;
  }

  public boolean isEmpty() {
    return remaining == 0;
  }

  public int remaining() {
    return remaining;
  }

  public boolean hasRemaining() {
    return remaining() > 0;
  }

  public byte peekByte() {
    return read(1).duplicate().get();
  }

  public short peekShort() {
    return read(2).duplicate().getShort();
  }

  public int peekInt() {
    return read(4).duplicate().getInt();
  }

  public long peekLong() {
    return read(8).duplicate().getLong();
  }

  public byte[] peekBytes(int size) {
    byte[] ret = new byte[size];
    read(size).duplicate().get(ret);
    return ret;
  }

  public int getInt() {
    int ret = read(4).getInt();
    remaining -= 4;
    return ret;
  }

  public char getByteChar() {
    char ret = (char) read(1).get();
    remaining--;
    return ret;
  }

  public int getShort() {
    int ret = read(2).getShort();
    remaining -= 2;
    return ret;
  }

  public byte get() {
    byte ret = read(1).get();
    remaining--;
    return ret;
  }

  public long getLong() {
    long ret = read(8).getLong();
    remaining -= 8;
    return ret;
  }

  public void get(byte[] bytes) {
    get(bytes, 0, bytes.length);
  }

  public void get(byte[] bytes, int offset, int length) {
    if (remaining() < length) throw new IllegalArgumentException("length");

    int need = length;
    while (need > 0) {
      ByteBuffer b = mBuffers.peek();

      int read = Math.min(b.remaining(), need);
      b.get(bytes, offset, read);
      need -= read;
      offset += read;
      if (b.remaining() == 0) {
        ByteBuffer removed = mBuffers.remove();
        assert b == removed;
        reclaim(b);
      }
    }

    remaining -= length;
  }

  public void get(ByteBufferList into, int length) {
    if (remaining() < length) throw new IllegalArgumentException("length");
    int offset = 0;

    while (offset < length) {
      ByteBuffer b = mBuffers.remove();
      int remaining = b.remaining();

      if (remaining == 0) {
        reclaim(b);
        continue;
      }

      if (offset + remaining > length) {
        int need = length - offset;
        ByteBuffer subset = obtain(need);
        subset.limit(need);
        b.get(subset.array(), 0, need);
        into.add(subset);
        mBuffers.addFirst(b);
        assert subset.capacity() >= need;
        assert subset.position() == 0;
        break;
      } else {
        into.add(b);
      }

      offset += remaining;
    }

    remaining -= length;
  }

  public void get(ByteBufferList into) {
    get(into, remaining());
  }

  public ByteBufferList get(int length) {
    ByteBufferList ret = new ByteBufferList();
    get(ret, length);
    return ret.order(order);
  }

  public ByteBuffer getAll() {
    if (remaining() == 0) return EMPTY_BYTEBUFFER;
    read(remaining());
    return remove();
  }

  private ByteBuffer read(int count) {
    if (remaining() < count) {
      throw new IllegalArgumentException("count : " + remaining() + "/" + count);
    }

    ByteBuffer first = mBuffers.peek();
    while (first != null && !first.hasRemaining()) {
      reclaim(mBuffers.remove());
      first = mBuffers.peek();
    }

    if (first == null) {
      return EMPTY_BYTEBUFFER;
    }

    if (first.remaining() >= count) {
      return first.order(order);
    }

    ByteBuffer ret = obtain(count);

    ret.limit(count);

    byte[] bytes = ret.array();
    int offset = 0;
    ByteBuffer bb = null;

    while (offset < count) {
      bb = mBuffers.remove();

      int toRead = Math.min(count - offset, bb.remaining());

      bb.get(bytes, offset, toRead);

      offset += toRead;

      if (bb.remaining() == 0) {
        reclaim(bb);
        bb = null;
      }
    }
    if (bb != null && bb.remaining() > 0) mBuffers.addFirst(bb);

    mBuffers.addFirst(ret);

    return ret.order(order);
  }

  public void trim() {
    read(0);
  }

  public void add(ByteBuffer b) {
    if (b.remaining() <= 0) {
      reclaim(b);
      return;
    }
    addRemaining(b.remaining());
    if (mBuffers.size() > 0) {
      ByteBuffer last = mBuffers.getLast();
      if (last.capacity() - last.limit() >= b.remaining()) {
        last.mark();
        last.position(last.limit());
        last.limit(last.capacity());
        last.put(b);
        last.limit(last.position());
        last.reset();
        reclaim(b);
        trim();
        return;
      }
    }
    mBuffers.add(b);
    trim();
  }

  public void addFirst(ByteBuffer b) {
    if (b.remaining() <= 0) {
      reclaim(b);
      return;
    }
    addRemaining(b.remaining());
    if (mBuffers.size() > 0) {
      ByteBuffer first = mBuffers.getFirst();
      if (first.position() >= b.remaining()) {
        first.position(first.position() - b.remaining());
        first.mark();
        first.put(b);
        first.reset();
        reclaim(b);
        return;
      }
    }
    mBuffers.addFirst(b);
  }

  private void addRemaining(int remaining) {
    if (this.remaining() >= 0) this.remaining += remaining;
  }

  public void recycle() {
    while (mBuffers.size() > 0) {
      reclaim(mBuffers.remove());
    }
    assert mBuffers.size() == 0;
    remaining = 0;
  }

  public ByteBuffer remove() {
    ByteBuffer ret = mBuffers.remove();
    remaining -= ret.remaining();
    return ret;
  }

  public int size() {
    return mBuffers.size();
  }

  public void spewString() {
    System.out.println(peekString());
  }

  public String peekString() {
    return peekString(null);
  }

  public String peekString(Charset charset) {
    if (charset == null) charset = US_ASCII;
    StringBuilder builder = new StringBuilder();
    for (ByteBuffer bb : mBuffers) {
      byte[] bytes;
      int offset;
      int length;
      if (bb.isDirect()) {
        bytes = new byte[bb.remaining()];
        offset = 0;
        length = bb.remaining();
        bb.get(bytes);
      } else {
        bytes = bb.array();
        offset = bb.arrayOffset() + bb.position();
        length = bb.remaining();
      }
      builder.append(new String(bytes, offset, length, charset));
    }
    return builder.toString();
  }

  public String readString() {
    return readString(null);
  }

  public String readString(Charset charset) {
    String ret = peekString(charset);
    recycle();
    return ret;
  }

  static class Reclaimer implements Comparator<ByteBuffer> {
    @Override
    public int compare(ByteBuffer byteBuffer, ByteBuffer byteBuffer2) {
      if (byteBuffer.capacity() == byteBuffer2.capacity()) return 0;
      if (byteBuffer.capacity() > byteBuffer2.capacity()) return 1;
      return -1;
    }
  }

  public void printList(){
    if(mBuffers != null ){
      Iterator<ByteBuffer> iterator = mBuffers.iterator();
      while (iterator.hasNext()){
        ByteBuffer buffer = iterator.next();
        LogUtils.d("buffer: "+ConvertUtils.bytes2HexString(buffer.array()));
      }
    }
  }
}
